feat: implement survey tracking and rendering#7
Conversation
- Add `Formbricks` widget that hosts a WebView, loads the surveys runtime and renders triggered surveys. - Add `Formbricks.track(code)` with code action-class lookup, a connectivity guard and trigger matching against cached surveys. - Add survey/action-class types, survey store, and WebView event/navigation/host plumbing, plus tests.
…any instance more easily
…focus - Build the WebViewController once in a StatefulWidget so a keyboard show no longer reloads the survey and makes it appear to close. - Bypass request caches for debug GETs (nocache param + Pragma) so newly created actions and surveys show up immediately. - Trim RN-port references and verbose internals from doc comments.
WalkthroughThis PR introduces a complete survey delivery system for the Formbricks Flutter SDK via WebView. It adds code-action tracking that resolves user-triggered actions to cached survey data, a survey state store managing active survey presentation, multi-language support utilities, and a secure WebView integration with navigation policy enforcement and event parsing. The SDK facade routes setup and tracking calls through a command queue, rendering surveys in a transparent modal when state changes occur. Supporting infrastructure includes HTML generation with JavaScript bridges, WebView controller hardening, survey type models with lossless JSON round-tripping, and comprehensive unit/widget tests. The playground app gains a manual connection UI for testing. Build system updates add dependencies for WebView, URL launching, and connectivity checking. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/playground/lib/main.dart`:
- Around line 215-222: The conditional rendering block checking if _connected &&
_status != 'connected' is incorrect because _status is set to 'connected ✓'
elsewhere (e.g., where connection success is assigned), causing the block to
always render; update the comparison to match the actual status value (either
compare to the same exact string used when setting _status like 'connected ✓' or
use a stable enum/constant for statuses) or change the check to something like
!_status.startsWith('connected') to avoid duplicate status output; adjust
references in the widget where _status is set and where the conditional uses
_connected and _status to ensure both use the same canonical status
representation (consider introducing a STATUS_CONNECTED constant or using
startsWith('connected')).
In `@apps/playground/test/widget_test.dart`:
- Around line 59-62: The test uses a fixed delay via tester.pump(const
Duration(milliseconds: 50)), which is flaky; replace that fixed-duration pump
with a deterministic wait such as await tester.pumpAndSettle() (or
tester.pumpAndSettle(timeout: ...) if needed) after the
tester.tap(find.text('Trigger Code Action')) so the test waits for
animations/async work to finish reliably instead of relying on a 50ms sleep.
In `@docs/TLS.md`:
- Around line 3-5: Update the documentation to use the published Flutter package
name: replace references to "formbricks" when referring to the Flutter SDK with
the correct package name "formbricks_flutter" (e.g., change the phrase
"formbricks Flutter SDK" to "formbricks_flutter package" or "formbricks_flutter
Flutter SDK"). Search for occurrences of "formbricks" in this doc (including the
opening sentence and any install/integration sections) and ensure they
consistently refer to "formbricks_flutter" when describing the Flutter package
to avoid confusion.
In `@Makefile`:
- Around line 79-86: The Makefile’s format targets are non-deterministic and not
CI-enforced: update the format-docs and format-check targets and the format
invocation so markdown formatting is pinned and validated; change the format
target to call $(MAKE) format-docs, change format-docs from "npx -y prettier
--write \"**/*.md\"" to a pinned invocation like "npx prettier@2.8.8 --write
\"**/*.md\"" (choose your locked version), and add markdown validation to
format-check by running the pinned prettier with a check flag (e.g., "npx
prettier@2.8.8 --check \"**/*.md\"") so CI will fail on drift; reference the
Makefile targets format, format-docs, and format-check when making these edits.
In `@packages/formbricks_flutter/CHANGELOG.md`:
- Around line 1-4: Update the CHANGELOG.md to reflect the actual shipped SDK
behavior by either adding a new version entry (e.g., "## 0.0.2" or "Unreleased")
or revising the existing "## 0.0.1" entry: remove or update the phrase about "No
SDK behavior yet" and add concise bullet points describing the implemented
features such as survey tracking and WebView delivery (mentioning the
`welcome()` API only if its behavior changed), so the changelog accurately
documents the PR's functionality.
In `@packages/formbricks_flutter/lib/src/common/setup.dart`:
- Around line 65-73: The cooldown logic currently checks any cached error state
(`existing` / `existing.status`) regardless of which setup target was used, so
fixed credentials can still be blocked; update the check and the persistence to
scope cooldown to the specific setup target (e.g., include `appUrl` and
`workspaceId` or a derived `targetKey` in the stored status). Concretely: when
persisting the status (the code around where status is saved), attach the target
identifiers to the stored record; when reading `existing` before returning
`SetupCooldownError` (the block using `existing.status`, `expiresAt`, and
`isNowExpired`), verify the stored target matches the current
`appUrl`/`workspaceId` (or `targetKey`) and only enforce the cooldown if it
matches; otherwise ignore the stale status and proceed with setup. Ensure
`SetupCooldownError(retryAt: expiresAt)` remains unchanged but is only returned
for matching targets.
In `@packages/formbricks_flutter/lib/src/common/utils.dart`:
- Around line 29-31: Normalize the survey language code before comparing by
lowercasing the language code as well as the alias: in utils.dart where the
matching occurs (the condition using l.language.code and l.language.alias
compared to the local variable lower), change the comparison to use
l.language.code?.toLowerCase() (null-safe) and keep
l.language.alias?.toLowerCase(), so getLanguageCode (and the selection logic
that sets selected) will match regardless of case.
In `@packages/formbricks_flutter/lib/src/survey/action.dart`:
- Around line 75-80: The NetworkError created in action.dart (returned via
Result.err) incorrectly uses HTTP 500 for an offline condition; update the
offline path in the code that constructs NetworkError (the Result.err(...) call)
to avoid using 500 — set status to 0 or null (or add an explicit offline/nonHttp
boolean on NetworkError) and leave the URL/Uri.tryParse(...) as-is; ensure
callers that inspect NetworkError.status treat 0/null (or the new offline flag)
as “no network” rather than a server error.
- Around line 109-113: The catch block in action.dart currently maps every
exception to NetworkError; instead, update the handler in the function using
Logger.error / Result.err to preserve typed errors: if the caught exception is a
FormbricksError return Result.err(e) (preserving its type and message),
otherwise wrap unknown exceptions in a distinct internal error type (e.g.,
InternalError or a new OperationError) that includes the original exception and
stack/context, and log the full exception when calling Logger.error; do not
collapse all failures into NetworkError.
In `@packages/formbricks_flutter/lib/src/survey/survey_store.dart`:
- Around line 25-30: The guard in setSurvey only compares survey.id so updates
that change survey content but keep the same id are dropped; update setSurvey
(the method) to compare the whole survey object instead of just id (e.g., check
_notifier.value != survey) or remove the id-only guard and always assign
_notifier.value = survey so listeners receive refreshed payloads; ensure you use
TSurvey equality (override ==/hashCode on the model if needed) when using object
inequality.
In `@packages/formbricks_flutter/lib/src/widgets/default_webview_host.dart`:
- Around line 120-136: The unawaited(_hardenAndLoad(_controller, widget.html))
call can surface unhandled async errors from the WebViewController operations;
modify _hardenAndLoad so it catches PlatformException/Exception around its
platform checks and calls (platform.setAllowFileAccess, setAllowContentAccess,
clearCache, clearLocalStorage, loadHtmlString) and handles failures (log via
debugPrint or the project's logger and return gracefully) so no exception
escapes as an unhandled future; alternatively, keep the caller but change the
unawaited invocation to unawaited(_hardenAndLoad(...).catchError((e, s) => /*
log/report */)) to ensure all errors from _hardenAndLoad are observed and
handled.
In `@packages/formbricks_flutter/lib/src/widgets/formbricks_widget.dart`:
- Around line 124-136: The _setup() method currently ignores the Result returned
by Formbricks.setup; change it to capture the Result (e.g., var res = await
Formbricks.setup(...)) and handle the Err case: if res.isErr (or matches Err),
log the error detail via Logger.error/Logger.debug including the err value, set
the widget state to a failed/initialized flag (or rethrow the error) to prevent
silent silent not_setup behavior, and only proceed when res.isOk; update
references to _setup, Formbricks.setup, and Logger.debug accordingly.
In `@packages/formbricks_flutter/lib/src/widgets/webview_navigation.dart`:
- Around line 33-42: The origin comparison in isAllowedWebViewNavigation uses
_origin to stringify the URI and treats explicit default ports (e.g., :443) as
different from omitted ports; update _origin to normalize default ports by
omitting the port when it equals the scheme's default (80 for http, 443 for
https) so that URIs like https://host and https://host:443 produce the same
origin; modify the _origin helper (referenced by isAllowedWebViewNavigation) to
compute an effective port and only include the :port suffix when a non-default
port is present.
In `@packages/formbricks_flutter/test/widgets/formbricks_widget_test.dart`:
- Around line 80-83: Replace the timing-sensitive manual delays by using
deterministic widget-test settling: where you currently call tester.runAsync(()
=> Future<void>.delayed(...)) followed by tester.pump(), change to
tester.pumpAndSettle() (or a pumpUntil-condition) to wait for frames and
microtasks to complete; update each occurrence of
tester.runAsync/Future<void>.delayed + tester.pump (including the other similar
blocks using tester.runAsync) to use tester.pumpAndSettle() so tests are stable
on slow CI.
In `@packages/formbricks_flutter/test/widgets/survey_webview_test.dart`:
- Around line 199-203: Replace the brittle fixed 50ms sleeps inside the
tester.runAsync blocks (where host.onEvent!(const DisplayCreatedEvent()) is
called and then you await Future.delayed(...)) with a deterministic polling wait
that repeatedly calls the side‑effect check (_storedUserData() or other expected
predicate) until it returns the expected result or a timeout elapses; implement
a small helper (e.g., waitForCondition or pumpUntil) used by the tests to poll
_storedUserData() and/or the storedData predicate with a short interval and a
sensible timeout, then swap the Future.delayed calls in the blocks around
host.onEvent!/DisplayCreatedEvent (and the same pattern at the other
occurrences) to await this helper so tests become deterministic and CI‑stable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 17270625-6126-4158-8049-4c5fe995f487
⛔ Files ignored due to path filters (1)
pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (45)
.gitignoreMakefileREADME.mdapps/playground/android/app/src/main/AndroidManifest.xmlapps/playground/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.mdapps/playground/lib/main.dartapps/playground/test/widget_test.dartdocs/FLUTTER_SDK_PLAN.mddocs/TLS.mdpackages/formbricks_flutter/CHANGELOG.mdpackages/formbricks_flutter/README.mdpackages/formbricks_flutter/lib/formbricks_flutter.dartpackages/formbricks_flutter/lib/src/common/api_client.dartpackages/formbricks_flutter/lib/src/common/logger.dartpackages/formbricks_flutter/lib/src/common/setup.dartpackages/formbricks_flutter/lib/src/common/survey_script_url.dartpackages/formbricks_flutter/lib/src/common/utils.dartpackages/formbricks_flutter/lib/src/survey/action.dartpackages/formbricks_flutter/lib/src/survey/survey_store.dartpackages/formbricks_flutter/lib/src/types/action_class.dartpackages/formbricks_flutter/lib/src/types/config.dartpackages/formbricks_flutter/lib/src/types/survey.dartpackages/formbricks_flutter/lib/src/widgets/default_webview_host.dartpackages/formbricks_flutter/lib/src/widgets/formbricks_widget.dartpackages/formbricks_flutter/lib/src/widgets/survey_html.dartpackages/formbricks_flutter/lib/src/widgets/survey_webview.dartpackages/formbricks_flutter/lib/src/widgets/webview_event.dartpackages/formbricks_flutter/lib/src/widgets/webview_navigation.dartpackages/formbricks_flutter/pubspec.yamlpackages/formbricks_flutter/test/common/api_client_test.dartpackages/formbricks_flutter/test/common/logger_test.dartpackages/formbricks_flutter/test/common/setup_test.dartpackages/formbricks_flutter/test/common/survey_script_url_test.dartpackages/formbricks_flutter/test/common/utils_test.dartpackages/formbricks_flutter/test/survey/action_test.dartpackages/formbricks_flutter/test/survey/survey_store_test.dartpackages/formbricks_flutter/test/types/action_class_test.dartpackages/formbricks_flutter/test/types/config_test.dartpackages/formbricks_flutter/test/types/survey_test.dartpackages/formbricks_flutter/test/widgets/default_webview_host_test.dartpackages/formbricks_flutter/test/widgets/formbricks_widget_test.dartpackages/formbricks_flutter/test/widgets/survey_html_test.dartpackages/formbricks_flutter/test/widgets/survey_webview_test.dartpackages/formbricks_flutter/test/widgets/webview_event_test.dartpackages/formbricks_flutter/test/widgets/webview_navigation_test.dart
…vior, tests, docs, Makefile, and Sonar config. - Scoped setup cooldowns to the failed `appUrl` + `workspaceId`. - Added `InternalError` / `internal_error`; offline tracking now uses `NetworkError.status == 0`. - Fixed language-code matching, same-id survey refreshes, setup error logging, WebView init error handling, and default-port origin checks. - Refactored `parseWebViewEvents()` to reduce complexity and remove duplicated parse-error literals. - Pinned Prettier in `Makefile`, added `format-docs-check`, updated TLS docs/changelog, and added targeted Sonar ignores for the required WebView JS bridge rules. - Replaced fixed 50ms widget-test waits with deterministic settling/polling.
|
pandeymangg
left a comment
There was a problem hiding this comment.
looks good and works really well with all the cases, I tested with different placement options and outside click etc on both iphone and ipad in the simulator and everything works fine 🙌
I just left some small comments, please take a look 🙏
Also, I don't see any logs from the playground app that should be routed through the sdk, can you please also check this?
| _navigator = Navigator.of(context, rootNavigator: true); | ||
| final route = RawDialogRoute<void>( | ||
| barrierColor: const Color(0x00000000), | ||
| barrierDismissible: false, |
There was a problem hiding this comment.
since barrierDismissible is false here, if somehow the surveys package fails to load, this makes the entire app (tested on ios) get stuck here with a transparent webview. Reproduction in the below video in which I can't click on any button after the survey webview fails to load:
Screen.Recording.2026-06-08.at.15.19.47.mov
| try { | ||
| return TActionClass.fromJson((entry as Map).cast<String, dynamic>()); | ||
| } catch (e) { | ||
| Logger.error('Skipping malformed cached action class: $e'); |
There was a problem hiding this comment.
I think we can improve this error message to malformed action class in workspace state as its more clear than the "cache" term 🙏
similarly for the other logs of this nature like the malformed "cached" survey log above
| 'onResponseCreated', | ||
| 'onOpenExternalURL', | ||
| 'onFinished', | ||
| 'onFilePick', |
There was a problem hiding this comment.
We don't need the onFilePick event here as the survey webview already implements this 🙏



What does this PR do?
Adds
Formbricks.track(...), matches cached code actions to survey triggers, and renders matching in-app surveys through theFormbrickswidget. It also adds the WebView bridge for survey events, persists display/response state, supports language/styling/render options, improves debug cache-busting, and updates the playground to connect to any instance and trigger code actions manually.Fixes ENG-1127
How should this be tested?
make check.